const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const t = require('@babel/types')
const ejs = require('ejs')
const { SyncHook } = require('tapable')

class Compiler {
  constructor(config) {
    this.config = config

    // 入口文件路径
    this.entryId

    // 所有模块
    this.modules = {}

    this.entry = config.entry
    this.root = process.cwd()

    this.hooks = {
      entryOption: new SyncHook(),
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      afterPlugins: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook(),
    }

    // 如果传递了 plugins

    let plugins = this.config.plugins

    if (Array.isArray(plugins)) {
      plugins.forEach(plugin => {
        plugin.apply(this)
      })
    }
    this.hooks.afterPlugins.call()
  }

  getSource (modulePath) {
    const rules = this.config.module.rules
    let content = fs.readFileSync(modulePath, 'utf8')

    for (let i = 0; i < rules.length; i++) {
      let { test, use } = rules[i]
      let length = use.length - 1
      if (test.test(modulePath)) {
        function normalLoader () {
          let loader = require(use[length--])

          content = loader(content)

          if (length >= 0) {
            normalLoader()
          }
        }

        normalLoader()
      }
    }

    // let content = fs.readFileSync(modulePath, 'utf8')
    return content
  }

  // AST 解析语法书
  parse (source, parentPath) {
    const ast = babylon.parse(source)
    const dependencies = []

    traverse(ast, {
      CallExpression (p) {
        const node = p.node
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'

          // 模块引用名
          let moduleName = node.arguments[0].value

          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js')

          moduleName = './' + path.join(parentPath, moduleName); // 'src/a.js'

          dependencies.push(moduleName)

          node.arguments = [t.stringLiteral(moduleName)]
          // console.log('----------------\b\r', node, '\b\r')
        }
      }
    })

    let sourceCode = generator(ast).code
    // console.log(sourceCode, parentPath)
    return { sourceCode, dependencies }
  }

  buildModule (modulePath, isEntry) {
    const source = this.getSource(modulePath)
    const moduleName = './' + path.relative(this.root, modulePath)

    if (isEntry) {
      this.entryId = moduleName
    }
    // 改造 source 源码， 返回依赖列表
    const { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))

    // console.log(source, moduleName)

    this.modules[moduleName] = sourceCode

    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false)
    })
  }

  emitFile () {
    const main = path.join(this.config.output.path, this.config.output.filename)
    const templateString = this.getSource(path.join(__dirname, 'main.ejs'))
    const code = ejs.render(templateString, { entryId: this.entryId, modules: this.modules })

    this.assets = {}
    // 路径对应的代码
    this.assets[main] = code

    // 写入编译好的文件
    fs.writeFileSync(main, this.assets[main])
  }

  run () {
    this.hooks.run.call()
    this.hooks.compile.call()
    // 创建依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)
    this.hooks.afterCompile.call()

    // 发射文件，打包后的
    this.emitFile()
    this.hooks.emit.call()
    this.hooks.done.call()
  }
}

module.exports = Compiler
